﻿//------------------------------------------------------------------------------------------------
//
// Based on code from blog post:
// Adding some dynamic to C#: interface wrapping [http://www.liensberger.it/web/blog/?p=298]
//
//------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Reflection.Emit;
using System.Runtime.CompilerServices;
using System.Diagnostics;

namespace ExpressionEvaluator.Anonymous
{
    public class AnonymousTypeBuilder
    {
        /// <summary>
        /// Builds the type.
        /// </summary>
        /// <param name="name">The name.</param>
        /// <param name="propertyNames">The property names.</param>
        /// <returns>Anonymous type emulator.</returns>
        public static Type BuildType(string name, params string[] propertyNames)
        {
            var anonymousTypeBuilder = new AnonymousTypeBuilder(name, propertyNames);
            return anonymousTypeBuilder.GenerateAnonymousType();
        }

        /// <summary>
        /// Gets the property names.
        /// </summary>
        /// <value>The property names.</value>
        public string[] PropertyNames { get; private set; }

        public string Name { get; private set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="AnonymousTypeBuilder"/> class.
        /// </summary>
        /// <param name="propertyNames">The property names.</param>
        public AnonymousTypeBuilder(string name, params string[] propertyNames)
        {
            PropertyNames = propertyNames;
            Name = name;
        }

        public Type GenerateAnonymousType()
        {
            string assemblyCacheName = Name;
            foreach (var argumentName in PropertyNames)
            {
                assemblyCacheName += "<" + argumentName + ">";
            }
            // try to locate existing assembly in current domain assemblies
            var existingAssembly = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(assembly => assembly.GetName().Name == assemblyCacheName);
            if (existingAssembly != null)
            {
                return existingAssembly.GetType(Name);
            }
            else
            {
                // creates the assembly and module.
                var builder = AppDomain.CurrentDomain.DefineDynamicAssembly(new AssemblyName(assemblyCacheName),
                    AssemblyBuilderAccess.Run);
                var module = builder.DefineDynamicModule(Name);

                // create the type that is used to wrap the object given. This
                // type will also implement the interface.
                Type result = CreateType(module);
                return result;
            }
        }

        private Type CreateType(ModuleBuilder module)
        {
            AnonymousArgumentProcessor argumentsProcessor = new AnonymousArgumentProcessor(PropertyNames);
            // create the type that is used to wrap the object into the interface.
            var typeBuilder = module.DefineType(Name, TypeAttributes.BeforeFieldInit | TypeAttributes.Class |
                TypeAttributes.Public | TypeAttributes.Sealed);

            // add generic parameters
            var genericParameters = typeBuilder.DefineGenericParameters(argumentsProcessor.GetGenericParametersNames());
            argumentsProcessor.AssignGenericParameters(genericParameters);
            
            // create related fields
            argumentsProcessor.AssignFields(item =>
                    {
                        FieldBuilder field = typeBuilder.DefineField(item.GenericFieldName, item.GeneratedType, FieldAttributes.Private | FieldAttributes.InitOnly);
                        // add debugger browsable attribute
                        field.SetCustomAttribute(
                            new CustomAttributeBuilder(
                                typeof(DebuggerBrowsableAttribute).GetConstructor(new Type[] { typeof(DebuggerBrowsableState) }),
                                new object[] { DebuggerBrowsableState.Never }
                                ));
                        return field;
                    }
                );

            // create debugger display info string
            StringBuilder typeCaption = new StringBuilder();
            for (int i = 0; i < PropertyNames.Length; i++)
            {
                var argument = argumentsProcessor[i];

                typeCaption.AppendFormat("{0} {1} = {{{1}}}", (i == 0 ? "\\{ " : ", "), argument.Name);
            }
            typeCaption.Append(" }");

            // add debugger display attribute
            typeBuilder.SetCustomAttribute(
                new CustomAttributeBuilder(
                // get debugger display attribute
                    typeof(DebuggerDisplayAttribute).GetConstructor(new Type[] { typeof(string) }),
                // pass type caption as parameter
                    new object[] { typeCaption.ToString() },
                // for property type
                    new PropertyInfo[] { typeof(DebuggerDisplayAttribute).GetProperty("Type") },
                // set type string
                    new object[] { "<Anonymous Type>" }));

            // add compiler generated attribute
            typeBuilder.SetCustomAttribute(
                new CustomAttributeBuilder(
                    typeof(CompilerGeneratedAttribute).GetConstructor(Type.EmptyTypes), new object[] { }
                    ));

            // create the constructor for the type.
            DefineConstructor(typeBuilder, argumentsProcessor);

            // create the properties for arguments
            DefineProperties(typeBuilder, argumentsProcessor);
            // implement GetHashCode function
            DefineGetHashCode(typeBuilder, argumentsProcessor);
            // implement Equals function
            DefineEquals(typeBuilder, argumentsProcessor);
            // implement ToString function
            DefineToString(typeBuilder, argumentsProcessor);

            // create the final type. 
            return typeBuilder.CreateType();
        }

        /// <summary>
        /// Defines to string.
        /// </summary>
        /// <param name="typeBuilder">The type builder.</param>
        /// <param name="argumentsProcessor">The arguments processor.</param>
        private void DefineToString(TypeBuilder typeBuilder, AnonymousArgumentProcessor argumentsProcessor)
        {
            MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual;

            // create the new method.
            var methodBuilder = typeBuilder.DefineMethod("ToString", attributes,
                typeof(string), Type.EmptyTypes);

            // add debugger hidden attribute
            methodBuilder.SetCustomAttribute(
                new CustomAttributeBuilder(
                    typeof(DebuggerHiddenAttribute).GetConstructor(Type.EmptyTypes), new object[] { }
                    ));

            // get the IL generator to generate the required IL.
            ILGenerator il = methodBuilder.GetILGenerator(3);
            // create local variables
            il.DeclareLocal(typeof(StringBuilder));
            il.DeclareLocal(typeof(string));

            // create string builder
            il.Emit(OpCodes.Newobj, typeof(StringBuilder).GetConstructor(Type.EmptyTypes));
            il.Emit(OpCodes.Stloc_0);
            il.Emit(OpCodes.Ldloc_0);

            var gotoFalse = il.DefineLabel();
            var gotoTrue = il.DefineLabel();

            var stringBuilderAppendString = typeof(StringBuilder).GetMethod("Append", new Type[] { typeof(string) });
            var stringBuilderAppendObject = typeof(StringBuilder).GetMethod("Append", new Type[] { typeof(object) });
            // process arguments
            for (int i = 0; i < PropertyNames.Length; i++)
            {
                var argument = argumentsProcessor[i];

                il.Emit(OpCodes.Ldstr, (i == 0 ? "{ " : ", ") + argument.Name + " = ");
                il.Emit(OpCodes.Callvirt, stringBuilderAppendString);

                il.Emit(OpCodes.Pop);
                il.Emit(OpCodes.Ldloc_0);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldfld, argument.Field);
                il.Emit(OpCodes.Box, argument.GeneratedType);
                il.Emit(OpCodes.Callvirt, stringBuilderAppendObject);

                il.Emit(OpCodes.Pop);
                il.Emit(OpCodes.Ldloc_0);
            }
            il.Emit(OpCodes.Ldstr, " }");
            il.Emit(OpCodes.Callvirt, stringBuilderAppendString);

            il.Emit(OpCodes.Pop);
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Callvirt, typeof(object).GetMethod("ToString", Type.EmptyTypes));
            il.Emit(OpCodes.Stloc_1);
            il.Emit(OpCodes.Ldloc_1);
            il.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Defines the equals.
        /// </summary>
        /// <param name="typeBuilder">The type builder.</param>
        /// <param name="argumentsProcessor">The arguments processor.</param>
        private void DefineEquals(TypeBuilder typeBuilder, AnonymousArgumentProcessor argumentsProcessor)
        {
            MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual;

            // create the new method.
            var methodBuilder = typeBuilder.DefineMethod("Equals", attributes,
                typeof(Boolean), new Type[] { typeof(object) });

            // add debugger hidden attribute
            methodBuilder.SetCustomAttribute(
                new CustomAttributeBuilder(
                    typeof(DebuggerHiddenAttribute).GetConstructor(Type.EmptyTypes), new object[] { }
                    ));

            // get the IL generator to generate the required IL.
            ILGenerator il = methodBuilder.GetILGenerator(3);
            // create local variables
            il.DeclareLocal(typeBuilder);
            il.DeclareLocal(typeof(bool));

            // add base
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Isinst, typeBuilder);
            il.Emit(OpCodes.Stloc_0);
            il.Emit(OpCodes.Ldloc_0);

            var gotoFalse = il.DefineLabel();
            var gotoTrue = il.DefineLabel();

            // process arguments
            for (int i = 0; i < PropertyNames.Length; i++)
            {
                var argument = argumentsProcessor[i];

                il.Emit(OpCodes.Brfalse, gotoFalse);
                Type equalityComparer = typeof(EqualityComparer<>).MakeGenericType(argument.GeneratedType);
                MethodInfo getDefault = TypeBuilder.GetMethod(equalityComparer, typeof(EqualityComparer<>).GetProperty("Default").GetGetMethod());
                il.Emit(OpCodes.Call, getDefault);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldfld, argument.Field);
                il.Emit(OpCodes.Ldloc_0);
                il.Emit(OpCodes.Ldfld, argument.Field);
                MethodInfo getEquals = TypeBuilder.GetMethod(equalityComparer, typeof(EqualityComparer<>).GetMethod("Equals", new Type[] { typeof(EqualityComparer<>).GetGenericArguments()[0], typeof(EqualityComparer<>).GetGenericArguments()[0] }));
                il.Emit(OpCodes.Callvirt, getEquals);
            }
            il.Emit(OpCodes.Br_S, gotoTrue);
            il.MarkLabel(gotoFalse);
            il.Emit(OpCodes.Ldc_I4_0);
            il.MarkLabel(gotoTrue);
            il.Emit(OpCodes.Stloc_1);
            il.Emit(OpCodes.Ldloc_1);
            il.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Defines the get hash code.
        /// </summary>
        /// <param name="typeBuilder">The type builder.</param>
        /// <param name="argumentsProcessor">The arguments processor.</param>
        private void DefineGetHashCode(TypeBuilder typeBuilder, AnonymousArgumentProcessor argumentsProcessor)
        {
            MethodAttributes attributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Virtual;

            // create the new method.
            var methodBuilder = typeBuilder.DefineMethod("GetHashCode", attributes,
                typeof(Int32), Type.EmptyTypes);

            // add debugger hidden attribute
            methodBuilder.SetCustomAttribute(
                new CustomAttributeBuilder(
                    typeof(DebuggerHiddenAttribute).GetConstructor(Type.EmptyTypes), new object[] { }
                    ));

            // get the IL generator to generate the required IL.
            ILGenerator il = methodBuilder.GetILGenerator();
            // create local variables
            il.DeclareLocal(typeof(Int32));
            il.DeclareLocal(typeof(Int32));

            // add base
            il.Emit(OpCodes.Ldc_I4, -1688775704);
            il.Emit(OpCodes.Stloc_0);

             // process arguments
            for (int i = 0; i < PropertyNames.Length; i++)
            {
                var argument = argumentsProcessor[i];
                il.Emit(OpCodes.Ldc_I4, -1521134295);
                il.Emit(OpCodes.Ldloc_0);
                il.Emit(OpCodes.Mul);
                Type equalityComparer = typeof(EqualityComparer<>).MakeGenericType(argument.GeneratedType);
                MethodInfo getDefault = TypeBuilder.GetMethod(equalityComparer, typeof(EqualityComparer<>).GetProperty("Default").GetGetMethod());
                il.Emit(OpCodes.Call, getDefault);
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldfld, argument.Field);
                MethodInfo getHashCode = TypeBuilder.GetMethod(equalityComparer, typeof(EqualityComparer<>).GetMethod("GetHashCode", new Type[] { typeof(EqualityComparer<>).GetGenericArguments()[0] }));
                il.Emit(OpCodes.Callvirt, getHashCode);
                il.Emit(OpCodes.Add);
                il.Emit(OpCodes.Stloc_0);

            }
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Stloc_1);
            il.Emit(OpCodes.Ldloc_0);
            il.Emit(OpCodes.Ret);
        }

        /// <summary>
        /// Defines the properties.
        /// </summary>
        /// <param name="typeBuilder">The type builder.</param>
        /// <param name="argumentsProcessor">The arguments processor.</param>
        private void DefineProperties(TypeBuilder typeBuilder, AnonymousArgumentProcessor argumentsProcessor)
        {
            // The property "get" methods require a special set of attributes.
            MethodAttributes getAttr = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig;

            // process arguments
            for (int i = 0; i < PropertyNames.Length; i++)
            {
                var argument = argumentsProcessor[i];

                // create property
                var propertyBuilder = typeBuilder.DefineProperty(argument.Name, PropertyAttributes.HasDefault,
                                argument.GeneratedType, null);
                // create the get method for the property.
                var getmethod = typeBuilder.DefineMethod("get_" + argument.Name, getAttr,
                    argument.GeneratedType, Type.EmptyTypes);

                // get the IL generator to generate the required IL.
                ILGenerator il = getmethod.GetILGenerator();

                // load the first argument (the instance itself) and the field.
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldfld, argument.Field);

                il.Emit(OpCodes.Ret);

                // set the method.
                propertyBuilder.SetGetMethod(getmethod);
            }
        }

        /// <summary>
        /// Defines the constructor.
        /// </summary>
        /// <param name="typeBuilder">The type builder.</param>
        /// <param name="argumentsProcessor">The arguments processor.</param>
        private void DefineConstructor(TypeBuilder typeBuilder, AnonymousArgumentProcessor argumentsProcessor)
        {
            ConstructorBuilder constructor = typeBuilder.DefineConstructor(MethodAttributes.Public |
                MethodAttributes.HideBySig, CallingConventions.Standard, argumentsProcessor.GetGenericsParameters());

            // add debugger hidden attribute
            constructor.SetCustomAttribute(
                new CustomAttributeBuilder(
                    typeof(DebuggerHiddenAttribute).GetConstructor(Type.EmptyTypes), new object[] { }
                    ));
            // process arguments
            for (int i = 0; i < PropertyNames.Length; i++)
            {
                // change name of contructor parameter
                constructor.DefineParameter(i + 1, ParameterAttributes.None, argumentsProcessor[i].Name);
            }
            // create the constructor.
            ILGenerator constructorIL = constructor.GetILGenerator();
            // call the base constructor. in this case of object.
            constructorIL.Emit(OpCodes.Ldarg_0);
            constructorIL.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));

            // process arguments
            for (int i = 0; i < PropertyNames.Length; i++)
            {
                // add initialize values
                constructorIL.Emit(OpCodes.Ldarg_0);
                constructorIL.Emit(OpCodes.Ldarg, i + 1);
                constructorIL.Emit(OpCodes.Stfld, argumentsProcessor[i].Field);
            }
            constructorIL.Emit(OpCodes.Ret);
        }

    }
}
